/* 
* Copyright 2004-2005 OpenSymphony 
* 
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License. You may obtain a copy 
* of the License at 
* 
*   http://www.apache.org/licenses/LICENSE-2.0 
*   
* Unless required by applicable law or agreed to in writing, software 
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 
* License for the specific language governing permissions and limitations 
* under the License.
* 
*/
using System;
using System.Globalization;
using System.Reflection;

using Common.Logging;

using Quartz.Spi;
using Quartz.Util;

namespace Quartz.Simpl
{
	/// <summary> 
	/// A JobFactory that instantiates the Job instance (using the default no-arg
	/// constructor, or more specifically: <see cref="ObjectUtils.InstantiateType" />), and
	/// then attempts to set all values in the <see cref="JobExecutionContext" />'s
	/// <see cref="JobDataMap" /> onto bean properties of the <see cref="IJob" />.
	/// </summary>
	/// <seealso cref="IJobFactory" />
	/// <seealso cref="SimpleJobFactory" />
	/// <seealso cref="JobExecutionContext.MergedJobDataMap" />
	/// <seealso cref="WarnIfPropertyNotFound" />
	/// <seealso cref="ThrowIfPropertyNotFound" />
	/// <author>James Houser</author>
	/// <author>Marko Lahma (.NET)</author>
	public class PropertySettingJobFactory : SimpleJobFactory
	{
		private static readonly ILog Log = LogManager.GetLogger(typeof (PropertySettingJobFactory));

		/// <summary> 
		/// Whether the JobInstantiation should fail and throw and exception if
		/// a key (name) and value (type) found in the JobDataMap does not 
		/// correspond to a proptery setter on the Job class.
		/// </summary>
		public virtual bool ThrowIfPropertyNotFound
		{
			get { return throwIfNotFound; }
			set { throwIfNotFound = value; }
		}

		/// <summary> 
		/// Get or set whether a warning should be logged if
		/// a key (name) and value (type) found in the JobDataMap does not 
		/// correspond to a proptery setter on the Job class.
		/// </summary>
		public virtual bool WarnIfPropertyNotFound
		{
			get { return warnIfNotFound; }
			set { warnIfNotFound = value; }
		}

		private bool warnIfNotFound = true;
		private bool throwIfNotFound = false;

		/// <summary>
		/// Called by the scheduler at the time of the trigger firing, in order to
		/// produce a <see cref="IJob" /> instance on which to call Execute.
		/// </summary>
		/// <remarks>
		/// <p>
		/// It should be extremely rare for this method to throw an exception -
		/// basically only the the case where there is no way at all to instantiate
		/// and prepare the Job for execution.  When the exception is thrown, the
		/// Scheduler will move all triggers associated with the Job into the
		/// <see cref="TriggerState.Error" /> state, which will require human
		/// intervention (e.g. an application restart after fixing whatever
		/// configuration problem led to the issue wih instantiating the Job.
		/// </p>
        /// </remarks>
		/// <param name="bundle">The TriggerFiredBundle from which the <see cref="JobDetail" />
		/// and other info relating to the trigger firing can be obtained.</param>
		/// <returns>the newly instantiated Job</returns>
		/// <throws>  SchedulerException if there is a problem instantiating the Job. </throws>
		public override IJob NewJob(TriggerFiredBundle bundle)
		{
			IJob job = base.NewJob(bundle);

			JobDataMap jobDataMap = new JobDataMap();
			jobDataMap.PutAll(bundle.JobDetail.JobDataMap);
			jobDataMap.PutAll(bundle.Trigger.JobDataMap);

			SetObjectProperties(job, jobDataMap);

			return job;
		}

        /// <summary>
        /// Sets the object properties.
        /// </summary>
        /// <param name="obj">The object to set properties to.</param>
        /// <param name="data">The data to set.</param>
		public virtual void SetObjectProperties(object obj, JobDataMap data)
		{
			Type paramType = null;

			foreach (string name in data.Keys)
			{
				string c = name.Substring(0, 1).ToUpper(CultureInfo.InvariantCulture);
				string propName = c + name.Substring(1);

				object o = data[name];
				PropertyInfo prop = obj.GetType().GetProperty(propName);

				try
				{
					if (prop == null)
					{
						HandleError(string.Format(CultureInfo.InvariantCulture, "No property on Job class {0} for property '{1}'", obj.GetType(), name));
						continue;
					}

					paramType = prop.PropertyType;

					if (o == null && (paramType.IsPrimitive || paramType.IsEnum))
					{
						// cannot set null to these
						HandleError(string.Format(CultureInfo.InvariantCulture, "Cannot set null to property on Job class {0} for property '{1}'", obj.GetType(), name));
					}
					if (paramType == typeof(char) && o!= null && o is string && ((string) o).Length != 1)
					{
						// handle special case
						HandleError(string.Format(CultureInfo.InvariantCulture, "Cannot set empty string to char property on Job class {0} for property '{1}'", obj.GetType(), name));
					}
					
					object goodValue = ObjectUtils.ConvertValueIfNecessary(paramType, o);
					prop.GetSetMethod().Invoke(obj, new object[] {goodValue});
				}
				catch (FormatException nfe)
				{
					HandleError(
							string.Format(CultureInfo.InvariantCulture, "The setter on Job class {0} for property '{1}' expects a {2} but was given {3}", obj.GetType(), name, paramType, o), nfe);

					continue;
				}
				catch (MethodAccessException)
				{
                    HandleError(string.Format(CultureInfo.InvariantCulture, "The setter on Job class {0} for property '{1}' expects a {2} but was given a {3}", obj.GetType(), name, paramType, o.GetType()));

					continue;
				}
				catch (ArgumentException e)
				{
					HandleError(
							string.Format(CultureInfo.InvariantCulture, "The setter on Job class {0} for property '{1}' expects a {2} but was given {3}", obj.GetType(), name, paramType, o.GetType()), e);

					continue;
				}
				catch (UnauthorizedAccessException e)
				{
					HandleError(
							string.Format(CultureInfo.InvariantCulture, "The setter on Job class {0} for property '{1}' could not be accessed.", obj.GetType(), name), e);
					continue;
				}
				catch (TargetInvocationException e)
				{
					HandleError(
							string.Format(CultureInfo.InvariantCulture, "The setter on Job class {0} for property '{1}' could not be accessed.", obj.GetType(), name), e);
					
					continue;
				}
			}
		}

		private void HandleError(string message)
		{
			HandleError(message, null);
		}

		private void HandleError(string message, Exception e)
		{
			if (ThrowIfPropertyNotFound)
			{
				throw new SchedulerException(message, e);
			}

			if (WarnIfPropertyNotFound)
			{
				if (e == null)
				{
					Log.Warn(message);
				}
				else
				{
					Log.Warn(message, e);
				}
			}
		}
	}
}
